/*
 * Copyright 2012 Krzysztof Otrebski (krzysztof.otrebski@gmail.com)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package pl.otros.vfs.browser.auth;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.vfs2.UserAuthenticationData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Map;

public class AuthStoreUtils {

  private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(AuthStoreUtils.class.getName());

  public static final String USER = "user";
  public static final String HOST = "host";
  public static final String PROTOCOL = "protocol";
  public static final String ENTRY = "Entry";
  public static final String USER_AUTHENTICATION_DATA = "UserAuthenticationData";
  public static final String TYPE = "Type";
  public static final String DATA = "Data";
  public static final String ALGORITHM_BLOW_FISH = "Blowfish";
  public static final int SALT_LENGTH = 64;
  private char[] password = null;

  private PasswordProvider passwordProvider;

  public AuthStoreUtils(PasswordProvider passwordProvider) {
    this.passwordProvider = passwordProvider;
  }

  public void save(AuthStore authStore, OutputStream out) throws IOException {
    Collection<UserAuthenticationInfo> all = authStore.getAll();
    for (UserAuthenticationInfo userAuthenticationInfo : all) {
      UserAuthenticationData userAuthenticationData = authStore.getUserAuthenticationData(userAuthenticationInfo);
    }
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder = null;
    try {
      documentBuilder = documentBuilderFactory.newDocumentBuilder();
      Document document = documentBuilder.newDocument();
      Element root = document.createElement("root");

      document.appendChild(root);
      for (UserAuthenticationInfo userAuthenticationInfo : all) {
        UserAuthenticationDataWrapper userAuthenticationData = authStore.getUserAuthenticationData(userAuthenticationInfo);
        Element entry = document.createElement(ENTRY);
        entry.setAttribute(PROTOCOL, userAuthenticationInfo.getProtocol());
        entry.setAttribute(HOST, userAuthenticationInfo.getHost());
        entry.setAttribute(USER, userAuthenticationInfo.getUser());
        Map<UserAuthenticationData.Type, char[]> addedTypes = userAuthenticationData.getAddedTypes();

        for (UserAuthenticationData.Type type : addedTypes.keySet()) {
          Element elementUserAuthenticationData = document.createElement(USER_AUTHENTICATION_DATA);
          char[] data = userAuthenticationData.getData(type);
          String value;
//          if (UserAuthenticationData.PASSWORD.equals(type)) {
//            if (password == null){
//              password = passwordProvider.getPassword("Enter password for password store");
//            }
//            if (password == null || password.length==0){
//              throw new IOException("Password for password store not entered");
//            }
//            value = saltAndEncrypt(data);
//          } else {
//            value = new String(data);
//          }
          value = new String(data);
          Element elementType = document.createElement(TYPE);
          elementType.setTextContent(type.toString());
          Element elementData = document.createElement(DATA);
          elementData.setTextContent(value);
          elementUserAuthenticationData.appendChild(elementType);
          elementUserAuthenticationData.appendChild(elementData);
          entry.appendChild(elementUserAuthenticationData);
        }
        root.appendChild(entry);
      }

      TransformerFactory transformerFactory = TransformerFactory.newInstance();

      Transformer transformer = transformerFactory.newTransformer();
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
      DOMSource source = new DOMSource(document);
      StreamResult result = new StreamResult(out);
      transformer.transform(source, result);
    } catch (Exception e) {
      throw new IOException(e);
    }

  }

  private String saltAndEncrypt(char[] data) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
      IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
    String value;
    Hex hex = new Hex();
    char[] saltedData = addSalt(data);

    byte[] encode = encrypt(saltedData, password);
    value = new String(hex.encode(encode));
    return value;
  }

  protected char[] addSalt(char[] data) {
    char[] saltedData = new char[SALT_LENGTH + data.length];
    for (int i = 0; i < SALT_LENGTH; i++) {
      saltedData[i] = 'a';// (char) random.nextInt();
    }
    for (int i = 0; i < data.length; i++) {
      saltedData[i + SALT_LENGTH] = data[i];
    }
    return saltedData;
  }

  protected char[] removeSalt(char[] data) {
    char[] deSalted = new char[data.length - SALT_LENGTH];
    System.arraycopy(data, SALT_LENGTH, deSalted, 0, deSalted.length);
    return deSalted;
  }

  public void load(AuthStore authStore, InputStream in) throws IOException {
    try {
      XMLReader xmlReader = XMLReaderFactory.createXMLReader();
      xmlReader.setContentHandler(new AuthStoreHandler(authStore));
      xmlReader.parse(new InputSource(in));
    } catch (SAXException e) {
      throw new IOException(e);
    }
  }

  private class AuthStoreHandler extends DefaultHandler {

    private UserAuthenticationDataWrapper userAuthenticationData;
    private UserAuthenticationInfo info;
    private StringBuilder sb = new StringBuilder();
    private String data;
    private String type;

    private AuthStore authStore;

    private AuthStoreHandler(AuthStore authStore) {
      this.authStore = authStore;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {

      if (ENTRY.equals(localName)) {
        userAuthenticationData = new UserAuthenticationDataWrapper();
        info = new UserAuthenticationInfo(atts.getValue(PROTOCOL), atts.getValue(HOST), atts.getValue(USER));

      }
      sb.setLength(0);

    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
      if (ENTRY.equals(localName)) {
        authStore.add(info, userAuthenticationData);
      } else if (USER_AUTHENTICATION_DATA.equals(localName)) {
//        if (UserAuthenticationData.PASSWORD.equals(new UserAuthenticationData.Type(type))) {
//          if (password == null){
//            password = passwordProvider.getPassword("Enter password for password store");
//          }
//          if (password == null || password.length==0){
//               throw new SAXException("Password for password store not entered");
//          }
//          Hex hex = new Hex();
//          try {
//            byte[] decode = (byte[]) hex.decode(data.trim());
//            byte[] decrypted = decrypt(decode, password);
//            char[] passwordWithSalt = bytesToChars(decrypted);
//            char[] password = removeSalt(passwordWithSalt);
//            data = new String(password);
//          } catch (Exception e) {
//            password=null;
//            throw new SAXException("Can't decrypt password", e);
//          }
//        }
        userAuthenticationData.setData(new UserAuthenticationData.Type(type), data.toCharArray());
      } else if (DATA.equals(localName)) {
        data = sb.toString();
      } else if (TYPE.equals(localName)) {
        type = sb.toString();
      }

      sb.setLength(0);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
      sb.append(ch, start, length);
    }
  }

  protected byte[] encrypt(char[] bytes, char[] password) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
      IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
    byte[] passBytes = charsToBytes(password);
    SecretKeySpec secretKeySpec = new SecretKeySpec(passBytes, ALGORITHM_BLOW_FISH);

    Cipher cipher = Cipher.getInstance(ALGORITHM_BLOW_FISH);

    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
    byte[] encrypted = cipher.doFinal(new String(bytes).getBytes("UTF-8"));
    return encrypted;
  }

  protected byte[] decrypt(byte[] bytes, char[] password) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
      IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
    byte[] passBytes = charsToBytes(password);
    SecretKeySpec secretKeySpec = new SecretKeySpec(passBytes, ALGORITHM_BLOW_FISH);
    Cipher cipher = Cipher.getInstance(ALGORITHM_BLOW_FISH);
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
    byte[] decrypted = cipher.doFinal(bytes);
    return decrypted;
  }

  protected byte[] charsToBytes(char[] chars) throws UnsupportedEncodingException {
    return new String(chars).getBytes("UTF-8");
  }

  protected char[] bytesToChars(byte[] bytes) {
    return new String(bytes, Charset.forName("UTF-8")).toCharArray();
  }

  public PasswordProvider getPasswordProvider() {
    return passwordProvider;
  }

  public void setPasswordProvider(PasswordProvider passwordProvider) {
    this.passwordProvider = passwordProvider;
  }

}
